package org.davepkxxx.jdbc

import scala.collection.{JavaConversions, mutable}
import javax.sql.DataSource
import java.sql.ResultSet
import scala.Int
import scala.beans.BeanProperty
import org.springframework.jdbc.core._
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations
import org.springframework.jdbc.support.KeyHolder
import scala.reflect.ClassTag

/**
 * The data model.
 * Created at 1/14/14 7:58 PM.
 * @define Coll `org.davepkxxx.jdbc.DataModel`
 * @define coll data model
 * @author David Dai
 */
abstract class DataModel extends mutable.HashMap[String, Any] {

  var tableName       = ""

  val modifiedNames   = mutable.HashSet[String]()

  val primaryKeys     = mutable.HashSet[String]()

  val getKeysWhenSave = true

  /**
   * Add a new field to this $coll, and record it's modified.
   * @param field the field to add.
   * @return the $coll itself
   */
  def :=(field: (String, Any)): this.type = { modifiedNames += field._1 ; this += field }

  /**
   * Add two or more fields to this $coll, and record they're modified.
   * @param field1 the first field to add.
   * @param field2 the second field to add.
   * @param fields the remaining fields to add.
   * @return the $coll itself
   */
  def :=(field1: (String, Any), field2: (String, Any), fields: (String, Any)*): this.type = this := field1 := field2 ::= fields

  /**
   * Add all fields produced by a TraversableOnce to this $coll, and record they're modified.
   * @param fields the TraversableOnce producing the fields to add.
   * @return the $coll itself.
   */
  def ::=(fields: TraversableOnce[(String, Any)]): this.type = {
    fields.seq foreach {case (name, value) => modifiedNames -= name }
    this ++= fields
  }

  /**
   * Remove a name from this $coll, and record it's unmodified.
   * @param name the name to be removed.
   * @return
   */
  override def -=(name: String): this.type = { modifiedNames -= name ; super.-=(name) }

  /**
   * Remove two or more names from this $coll, and record they're unmodified.
   * @param name1 the first name to be removed.
   * @param name2 the second name to be removed.
   * @param names the remaining names to be removed.
   * @return the $coll itself
   */
  override def -=(name1: String, name2: String, names: String*): this.type = this -= name1 -= name2 --= names

  /**
   * Remove all names produced by an iterator from this $coll, and record they're unmodified.
   * @param names the names to be removed.
   * @return
   */
  override def --=(names: TraversableOnce[String]): this.type = { modifiedNames --= names ; super.--=(names) }

  /**
   * Add a new field to this $coll, and record it's modified,
   * and optionally returns previously bound value.
   * @param name  the name is a field name.
   * @param value the value is a field value.
   */
  def set(name: String, value: Any): Option[Any] = { modifiedNames.add(name) ; put(name, value) }

  /**
   * Remove a name from this $coll, and record it's unmodified,
   * returning the value associated previously with that name as an option.
   * @param name the name to be removed.
   */
  override def remove(name: String): Option[Any] = { modifiedNames.remove(name) ; super.remove(name) }

  /**
   * Remove all fields and remove all modification record.
   */
  override def clear() { modifiedNames.clear() ; super.clear() }

  /**
   * Returns true if this $coll contains no modified field.
   * @return true if this $coll contains no modified field.
   */
  def isModified: Boolean = modifiedNames.isEmpty

}

/**
 * The default data model.
 * Created at 1/15/14 23:03 PM.
 * @param tabName the table name is a table name in database.
 * @param pks     the primary keys is the primary key names in database.
 * @author David Dai
 */
class DefaultDataModel(tabName: String, pks: Set[String]) extends DataModel {

  this.tableName = tabName
  this.primaryKeys ++= pks

  /**
   * The default data model, no primary key.
   * @param tableName the table name is a table name in database.
   */
  def this(tableName: String) = this(tableName, Set())

}

/**
 * Convenient super class for JDBC-based data access data models in Scala.
 * Created at 1/14/14 7:58 PM.
 * @author David Dai
 */
class DataModelDaoSupport extends ScalaJdbcDaoSupport {

  @BeanProperty var dataModelJdbcTemplate: DataModelJdbcTemplate = _

  override def initTemplateConfig: Unit = this.dataModelJdbcTemplate = new DataModelJdbcTemplate(getJdbcTemplate)

}

/**
 * Interface specifying a basic set of data model operations.
 * Created at 1/14/14 7:58 PM.
 * @author David Dai
 */
trait DataModelJdbcOperations {

  /**
   * Expose the classic Spring JdbcTemplate to allow invocation of classic JDBC operations.
   * @return the classic Spring JdbcTemplate.
   */
  def getJdbcOperations: JdbcOperations

  /**
   * Expose the classic NamedParameterJdbcOperations to allow invocation of classic Scala JDBC operations.
   * @return the classic NamedParameterJdbcOperations.
   */
  def getNamedParameterJdbcOperations: NamedParameterJdbcOperations

  /**
   * Expose the classic Scala JdbcTemplate to allow invocation of classic Scala JDBC operations.
   * @return the classic Scala JdbcTemplate.
   */
  def getScalaJdbcOperations: ScalaJdbcOperations

  /**
   * Execute a query given data model, mapping a single result row to a data model.
   * @param model the primary keys of model is query condition.
   * @tparam T    a class of mapping model.
   * @return the single mapped model.
   */
  def query[T <: DataModel](model: T): T

  /**
   * Issue a single save operation, via a prepared statement, binding the given data model.
   * @param model the modified fields of model is save data.
   */
  def save(model: DataModel): Option[KeyHolder]

  /**
   * Issue a single update operation, via a prepared statement, binding the given data model.
   * @param model the modified fields of model is update data, the primary keys of model is update condition.
   */
  def update(model: DataModel): Unit

  /**
   * Issue a single delete operation, via a prepared statement, binding the given data model.
   * @param model the primary keys of model is delete condition.
   */
  def delete(model: DataModel): Unit

}

/**
 * Template class with a basic set of data model operations.
 * Created at 1/14/14 7:58 PM.
 * @param classicJdbcTemplate the classic Spring JdbcTemplate to wrap
 * @author David Dai
 */
class DataModelJdbcTemplate(classicJdbcTemplate: JdbcOperations) extends DataModelJdbcOperations {

  private val classicScalaJdbcOperations          = new ScalaJdbcTemplate(classicJdbcTemplate)

  private val classicNamedParameterJdbcOperations = classicScalaJdbcOperations.getNamedParameterJdbcOperations

  /**
   * Template class with a basic set of data model operations, no data source.
   */
  def this() = this(new JdbcTemplate)

  /**
   * Template class with a basic set of data model operations
   * @param dataSource the JDBC DataSource to obtain connections from
   */
  def this(dataSource: DataSource) = this(new JdbcTemplate(dataSource))

  override def getJdbcOperations: JdbcOperations = classicJdbcTemplate

  override def getNamedParameterJdbcOperations: NamedParameterJdbcOperations = classicNamedParameterJdbcOperations

  override def getScalaJdbcOperations: ScalaJdbcOperations = classicScalaJdbcOperations

  override def query[T <: DataModel](model: T): T = {
    require(model.tableName.trim.isEmpty, "The table name is blank.")
    require(model.primaryKeys.isEmpty,    "The name of primary keys is empty.")
    val sql = new mutable.StringBuilder("SELECT * FROM ").append(model.tableName).append(where(model)).toString()
    getScalaJdbcOperations.queryForObject(sql, new DataModelRowMapper(model.getClass).mapFunction, model)
  }

  override def save(model: DataModel): Option[KeyHolder] = {
    require(model.tableName.trim.isEmpty, "The table name is blank.")
    require(!model.isModified,            "The data model is not modified.")
    val sql    = new mutable.StringBuilder("INSERT INTO ").append(model.tableName).append("(")
    val values = new mutable.StringBuilder(") VALUES(")
    model.modifiedNames.zipWithIndex.foreach {case (name, i) => {
      sql.append(if (i > 0) "," else "").append(name)
      values.append(if (i > 0) ",:" else ":").append(name)
    }}
    if (!model.getKeysWhenSave) {
      getScalaJdbcOperations.update(sql.append(values).append(")").toString(), model) ; None
    } else if (model.primaryKeys.isEmpty) {
      Some(getScalaJdbcOperations.save(sql.append(values).append(")").toString(), model))
    } else {
      val keys = model.primaryKeys.toArray(ClassTag(classOf[String]))
      Some(getScalaJdbcOperations.updateForKey(sql.append(values).append(")").toString(), model, keys))
    }
  }

  override def update(model: DataModel): Unit = {
    require(model.tableName.trim.isEmpty, "The table name is blank.")
    require(model.primaryKeys.isEmpty,    "The name of primary keys is empty.")
    require(!model.isModified,            "The data model is not modified.")
    val sql  = new mutable.StringBuilder("UPDATE ").append(model.tableName).append(" SET ")
    model.modifiedNames.zipWithIndex.foreach {case (name, i) => {
      sql.append(if (i > 0) "," else "").append(name).append("=:").append(name)
    }}
    getScalaJdbcOperations.update(sql.append(where(model)).toString(), model)
  }

  override def delete(model: DataModel): Unit = {
    require(model.tableName.trim.isEmpty, "The table name is blank.")
    require(model.primaryKeys.isEmpty,    "The name of primary keys is empty.")
    val sql = new mutable.StringBuilder("DELETE FROM ").append(model.tableName).append(where(model)).toString()
    getScalaJdbcOperations.update(sql, model)
  }

  private def where(model: DataModel): StringBuilder = {
    val sql = new StringBuilder(" WHERE ")
    model.primaryKeys.zipWithIndex.foreach {case (name, i) => {
      sql.append(if (i > 0) " AND " else "").append(name)
      if (model.get(name) eq null) sql.append(" IS NULL") else sql.append("=:").append(name)
    }}
    sql
  }

}

class DataModelRowCallbackHandler[T <: DataModel](cls: Class[T]) extends RowCountCallbackHandler {

  private val rowMapper = new DataModelRowMapper[T](cls)

  val results: mutable.Buffer[T] = mutable.ArrayBuffer[T]()

  override def processRow(rs: ResultSet, rowNum: Int): Unit = { results += rowMapper.mapRow(rs, rowNum) }

}

class DataModelRowMapper[T <: DataModel](cls: Class[T]) extends RowMapper[T] {

  private val rowMapper = new ColumnMapRowMapper

  val mapFunction = (rs: ResultSet, rowNum: Int) =>
    cls.newInstance ::= JavaConversions.mapAsScalaMap(rowMapper.mapRow(rs, rowNum)).toTraversable

  override def mapRow(rs: ResultSet, rowNum: Int): T = mapFunction(rs, rowNum)

}


